﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MVVMFramework.ViewModel
{
    /// <summary>
    /// Provides standard functions for working with browse of some entity. 
    /// The class should be inherited.
    /// </summary>
    /// <typeparam name="T">Entity for browse</typeparam>
    public abstract class BrowseEntitiesViewModel<T> : ViewModelBase, IRefreshable, IDataChangedController where T : class
    {
        private bool loaded;

        private readonly object cancelTaskLock = new object();
        private CancellationTokenSource tokenSourceLoading;

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntitiesViewModel&lt;T&gt;"/> class.
        /// </summary>
        protected BrowseEntitiesViewModel()
        {
            this.entities = new BrowseEntity<T>(CompareEntities);
            this.entities.CurrentChanged += entities_CurrentChanged;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntitiesViewModel&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="entities">The collection whose elements are used to fill to the Entities property.</param>
        protected BrowseEntitiesViewModel(IEnumerable<T> entities)
        {
            if (entities == null)
                throw new ArgumentNullException("entities");

            this.entities = new BrowseEntity<T>(entities, CompareEntities);
            this.entities.CurrentChanged += entities_CurrentChanged;
            IsAsyncLoading = false;
        }

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether this instance is async loading.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this view model is using async loading of the entities; otherwise, <c>false</c>.
        /// </value>
        public bool IsAsyncLoading
        {
            get;
            set;
        }

        private bool loading;
        /// <summary>
        /// Gets a value indicating whether entities are in the loading process at the moment.
        /// </summary>
        public bool Loading
        {
            get
            {
                return loading;
            }
            protected set
            {
                if (loading != value)
                {
                    loading = value;
                    OnPropertyChanged(() => Loading);
                }
            }
        }

        private string title;
        /// <summary>
        /// Gets or sets a title associated with the current view model.
        /// </summary>
        /// <value>
        /// The title.
        /// </value>
        public string Title
        {
            get
            {
                if (string.IsNullOrEmpty(title))
                    title = GetTitle();

                return title;
            }
            set
            {
                if (title != value)
                {
                    title = value;
                    OnPropertyChanged(() => Title);
                }
            }
        }

        private readonly BrowseEntity<T> entities;
        /// <summary>
        /// Gets the entities
        /// </summary>
        public BrowseEntity<T> Entities
        {
            get
            {
                return entities;
            }
        }

        /// <summary>
        /// Gets a value indicating can a new object to be inserted
        /// </summary>
        public virtual bool CanInsert
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// Gets a value indicating can the current object to be changed
        /// </summary>
        public virtual bool CanChange
        {
            get
            {
                return entities.Current != null && !loading;
            }
        }

        /// <summary>
        /// Gets a value indicating can the current object to be deleted
        /// </summary>
        public virtual bool CanDelete
        {
            get
            {
                return entities.Current != null && !loading;
            }
        }

        private string loadingError;
        /// <summary>
        /// Get the comment of an error occured when loading Warehouses
        /// </summary>
        public virtual string LoadingError
        {
            get
            {
                return loadingError;
            }
            protected set
            {
                if (loadingError != value)
                {
                    loadingError = value;
                    OnPropertyChanged(() => LoadingError);
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the loading has error
        /// </summary>
        public bool HasError
        {
            get
            {
                return !string.IsNullOrWhiteSpace(loadingError);
            }
        }

        #endregion

        /// <summary>
        /// Refreshes the collection of entity
        /// </summary>
        protected virtual void RefreshEntities(object state = null)
        {
            if (IsAsyncLoading)
                loadAsync(true, null, state);
            else
                load(true, state);
        }

        /// <summary>
        /// Called when refreshing entities.
        /// </summary>
        /// <param name="state">The state.</param>
        protected virtual void OnRefreshingEntities(object state)
        {

        }

        /// <summary>
        /// Called when entities have been refreshed.
        /// </summary>
        /// <param name="state">The state.</param>
        protected virtual void OnRefreshedEntities(object state)
        {
        }

        /// <summary>
        /// Called when [current entity changed]
        /// </summary>
        protected virtual void OnCurrentEntityChanged()
        {

        }

        /// <summary>
        /// Refreshes the entities.
        /// </summary>
        /// <param name="force">if set to <c>true</c> loading is forced.</param>
        /// <remarks></remarks>
        public void Refresh(bool force)
        {
            if (force)
                RefreshEntities();
            else if (!loaded)
                RefreshEntities();
        }

        #region Commands

        private ICommand insertCommand;
        /// <summary>
        /// Gets the insert command. Used to insert entity into the entities collection.
        /// </summary>
        public ICommand InsertCommand
        {
            get
            {
                return insertCommand = insertCommand ?? new RelayCommand(() =>
                {
                    if (CanInsert)
                    {
                        T resultEntity = null;
                        bool success = false;
                        try
                        {
                            success = RunRequest(null, Requests.Insert, out resultEntity);
                        }
                        catch (Exception ex)
                        {
                            if (!HandleRunRequestException(Requests.Insert, null, ex))
                                throw;
                        }

                        if (success)
                        {
                            if (IsAsyncLoading)
                            {
                                loadAsync(false, () => entities.SetCurrentAs(resultEntity));
                            }
                            else
                            {
                                load(false);
                                entities.SetCurrentAs(resultEntity);
                            }
                        }
                    }
                });
            }
        }

        private ICommand changeCommand;
        /// <summary>
        /// Gets the change command. Used to change current entity.
        /// </summary>
        public ICommand ChangeCommand
        {
            get
            {
                return changeCommand = changeCommand ?? new RelayCommand(() =>
                {
                    if (CanChange)
                    {
                        T entity = entities.Current;
                        if (entity != null)
                        {
                            bool success = false;
                            try
                            {
                                T resultEntity;
                                success = RunRequest(entity, Requests.Change, out resultEntity);
                            }
                            catch (Exception ex)
                            {
                                if (!HandleRunRequestException(Requests.Change, entity, ex))
                                    throw;
                            }

                            if (success)
                            {
                                if (IsAsyncLoading)
                                    loadAsync(true, null);
                                else
                                    load(true);
                            }
                        }
                    }
                });
            }
        }

        private ICommand deleteCommand;
        /// <summary>
        /// Gets the delete command. Used to delete current entity.
        /// </summary>
        public ICommand DeleteCommand
        {
            get
            {
                return deleteCommand = deleteCommand ?? new RelayCommand(() =>
                {
                    if (CanDelete)
                    {
                        T currentEntity = entities.Current;
                        if (currentEntity != null)
                        {
                            bool deleted = false;
                            int deletedIndex = entities.IndexOf(currentEntity);

                            try
                            {
                                deleted = DeleteEntity(currentEntity);
                            }
                            catch (Exception ex)
                            {
                                if (!HandleDeleteException(ex))
                                    throw;
                            }

                            if (deleted)
                            {
                                if (IsAsyncLoading)
                                {
                                    loadAsync(false, () => setDefaultPrevious(deletedIndex));
                                }
                                else
                                {
                                    load(false);
                                    setDefaultPrevious(deletedIndex);
                                }
                            }
                        }
                    }
                });
            }
        }

        private ICommand refreshCommand;
        /// <summary>
        /// Gets the refresh command. Used to reload entities.
        /// </summary>
        public ICommand RefreshCommand
        {
            get
            {
                return refreshCommand = refreshCommand ?? new RelayCommand(() =>
                {
                    if (CanExecuteRefreshCommand())
                        RefreshEntities();
                });
            }
        }

        #endregion

        #region Abstract/Virtual Methods

        /// <summary>
        /// Gets the title associated with the ViewModel
        /// </summary>
        /// <returns></returns>
        protected abstract string GetTitle();
        /// <summary>
        /// Loads the entities.
        /// </summary>
        /// <returns></returns>
        protected abstract IEnumerable<T> LoadEntities();
        /// <summary>
        /// Compares the entities. Used to compare two entities using business logic, for example by id or using group of properties.
        /// </summary>
        /// <param name="obj1">The first entity.</param>
        /// <param name="obj2">The second entity.</param>
        /// <returns></returns>
        protected abstract bool CompareEntities(T obj1, T obj2);
        /// <summary>
        /// Runs the request.
        /// </summary>
        /// <param name="entity">The entity which is involved in the request.</param>
        /// <param name="request">The request.</param>
        /// <param name="result">The result of the request.</param>
        /// <returns></returns>
        protected abstract bool RunRequest(T entity, Requests request, out T result);
        /// <summary>
        /// Deletes the entity.
        /// </summary>
        /// <param name="entity">The entity to be deleted.</param>
        /// <returns></returns>
        protected abstract bool DeleteEntity(T entity);
        /// <summary>
        /// Handles the load exception.
        /// </summary>
        protected virtual bool HandleLoadException(Exception ex)
        {
            LoadingError = GetExceptionMessage(ex, false);
            return false;
        }
        /// <summary>
        /// Handles the delete exception.
        /// </summary>
        /// <param name="ex">The exception which occurred during deleting entity.</param>
        /// <returns></returns>
        protected abstract bool HandleDeleteException(Exception ex);
        /// <summary>
        /// Handles the run request exception.
        /// </summary>
        /// <param name="request">The request in which the error occurred.</param>
        /// <param name="entity">The entity which is involved in the request.</param>
        /// <param name="ex">The exception which occurred during running request.</param>
        /// <returns></returns>
        protected abstract bool HandleRunRequestException(Requests request, T entity, Exception ex);

        protected virtual bool CanExecuteRefreshCommand()
        {
            return true;
        }

        #endregion

        #region Private Methods

        private void setDefaultPrevious(int index)
        {
            if (index >= entities.Count)
                index--;
            if (index >= 0)
                entities.Current = entities[index];
        }

        private void load(bool restoreCurrent, object state = null)
        {
            try
            {
                LoadingError = string.Empty;

                OnRefreshingEntities(state);

                Loading = true;
                IEnumerable<T> entries = LoadEntities();
                entities.Refill(entries, restoreCurrent);
                loaded = true;

                OnPropertyChanged(() => Entities);
                Loading = false;
                OnRefreshedEntities(state);
            }
            catch (Exception ex)
            {
                if (!HandleLoadException(ex))
                    throw;
            }
        }

        private Task loadAsync(bool restoreCurrent, Action complete, object state = null)
        {
            CancellationTokenSource tokenSource = new CancellationTokenSource();

            lock (cancelTaskLock)
            {
                if (tokenSourceLoading != null)
                    tokenSourceLoading.Cancel();

                tokenSourceLoading = tokenSource;
            }

            return Task.Factory.StartNew(() =>
            {
                LoadingError = string.Empty;

                OnRefreshingEntities(state);

                Loading = true;
                IEnumerable<T> entries = LoadEntities();
                return entries;

            }, tokenSource.Token).ContinueWith(t =>
            {
                Loading = false;

                if (t.IsFaulted)
                {
                    if (t.Exception != null)
                    {
                        Exception ex = t.Exception.InnerExceptions[0];
                        if (!HandleLoadException(ex))
                            throw ex;
                    }
                }
                else
                {
                    if (!t.IsCanceled)
                    {
                        entities.Refill(t.Result, restoreCurrent);
                        loaded = true;

                        OnPropertyChanged(() => Entities);
                        OnRefreshedEntities(state);
                    }
                }

                if (complete != null)
                    complete.Invoke();
            }, TaskContinuationOptions.ExecuteSynchronously);
            //}, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void entities_CurrentChanged(object sender, EventArgs e)
        {
            OnPropertyChanged(() => CanChange);
            OnPropertyChanged(() => CanDelete);
            OnCurrentEntityChanged();
        }

        #endregion

        /// <summary>
        /// Called when the ViewModel registers dependencies.
        /// </summary>
        protected override void OnRegisterDependencies()
        {
            base.OnRegisterDependencies();

            this.RegisterDependencies(() => Loading, () => CanChange);
            this.RegisterDependencies(() => Loading, () => CanDelete);
            this.RegisterDependencies(() => LoadingError, () => HasError);
        }

        #region IDataChangedController

        public void DataChanged()
        {
            RefreshEntities();
        }

        #endregion

    }

    public abstract class BrowseEntitiesViewModel<TBasic, TDependency> : BrowseEntitiesViewModel<TBasic>
        where TBasic : class
        where TDependency : class
    {
        private readonly object cancelTaskLock = new object();
        private CancellationTokenSource tokenSourceLoading;

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntitiesViewModel&lt;T&gt;"/> class.
        /// </summary>
        protected BrowseEntitiesViewModel()
            : base()
        {
            dependentEntities = new BrowseEntity<TDependency>(CompareEntities);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BrowseEntitiesViewModel&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="entities">The collection whose elements are used to fill to the Entities property.</param>
        protected BrowseEntitiesViewModel(IEnumerable<TBasic> entities)
            : base(entities)
        {
            dependentEntities = new BrowseEntity<TDependency>(CompareEntities);
        }

        #region Properties

        private readonly BrowseEntity<TDependency> dependentEntities;
        /// <summary>
        /// Gets the dependent entities
        /// </summary>
        public BrowseEntity<TDependency> DependentEntities
        {
            get
            {
                return dependentEntities;
            }
        }

        private bool loadingDependentEntities;
        public bool LoadingDependentEntities
        {
            get
            {
                return loadingDependentEntities;
            }
            protected set
            {
                if (loadingDependentEntities != value)
                {
                    loadingDependentEntities = value;
                    OnPropertyChanged(() => LoadingDependentEntities);
                }
            }
        }

        private string loadingDependentEntitiesError;

        public string LoadingDependentEntitiesError
        {
            get
            {
                return loadingDependentEntitiesError;
            }
            protected set
            {
                if (loadingDependentEntitiesError != value)
                {
                    loadingDependentEntitiesError = value;
                    OnPropertyChanged(() => LoadingDependentEntitiesError);
                }
            }
        }

        public bool IsAsyncDependentEntitiesLoading
        {
            get;
            set;
        }

        #endregion

        protected override void OnCurrentEntityChanged()
        {
            refreshDependentEntities(Entities.Current);

            base.OnCurrentEntityChanged();
        }

        #region Abstracts

        /// <summary>
        /// Loads the dependent entities.
        /// </summary>
        /// <returns></returns>
        protected abstract IEnumerable<TDependency> LoadDependentEntities(TBasic basicEntity);

        /// <summary>
        /// Compares the entities. Used to compare two entities using business logic, for example by id or using group of properties.
        /// </summary>
        /// <param name="obj1">The first entity.</param>
        /// <param name="obj2">The second entity.</param>
        /// <returns></returns>
        protected abstract bool CompareEntities(TDependency obj1, TDependency obj2);

        #endregion

        #region Protected

        /// <summary>
        /// Called when refreshing dependent entities.
        /// </summary>
        /// <param name="state">The state.</param>
        protected virtual void OnRefreshingDependentEntities(object state)
        {

        }

        /// <summary>
        /// Called when dependent entities have been refreshed.
        /// </summary>
        /// <param name="state">The state.</param>
        protected virtual void OnRefreshedDependentEntities(object state)
        {
        }

        /// <summary>
        /// Handles the load exception.
        /// </summary>
        protected virtual bool HandleLoadDenendentEntitiesException(Exception ex)
        {
            LoadingDependentEntitiesError = GetExceptionMessage(ex, false);
            return false;
        }

        #endregion

        #region Public

        public void RefreshDependentEntities(object state = null)
        {
            refreshDependentEntities(Entities.Current, state);
        }

        #endregion

        /// <summary>
        /// Refreshes the dependent entities.
        /// </summary>
        /// <remarks></remarks>
        private void refreshDependentEntities(TBasic currentBasic, object state = null)
        {
            if (currentBasic == null)
            {
                dependentEntities.Clear();
                return;
            }

            if (IsAsyncDependentEntitiesLoading)
                loadDependentEntitiesAsync(currentBasic, true, null, state);
            else
                loadDependentEntities(currentBasic, true, state);
        }

        private void loadDependentEntities(TBasic currentBasicEntity, bool restoreCurrent, object state = null)
        {
            try
            {
                LoadingDependentEntitiesError = string.Empty;

                OnRefreshingDependentEntities(state);

                LoadingDependentEntities = true;
                IEnumerable<TDependency> dependentEntries = LoadDependentEntities(currentBasicEntity);
                dependentEntities.Refill(dependentEntries, restoreCurrent);

                OnPropertyChanged(() => DependentEntities);
                LoadingDependentEntities = false;
                OnRefreshedDependentEntities(state);
            }
            catch (Exception ex)
            {
                if (!HandleLoadDenendentEntitiesException(ex))
                    throw;
            }
        }

        private Task loadDependentEntitiesAsync(TBasic currentBasic, bool restoreCurrent, Action complete, object state = null)
        {
            CancellationTokenSource tokenSource = new CancellationTokenSource();

            lock (cancelTaskLock)
            {
                if (tokenSourceLoading != null)
                    tokenSourceLoading.Cancel();

                tokenSourceLoading = tokenSource;
            }

            return Task.Factory.StartNew(() =>
            {
                LoadingDependentEntitiesError = string.Empty;

                OnRefreshingDependentEntities(state);

                LoadingDependentEntities = true;
                IEnumerable<TDependency> entities = LoadDependentEntities(currentBasic);
                return entities;

            }, tokenSource.Token).ContinueWith(t =>
            {
                LoadingDependentEntities = false;

                if (t.IsFaulted)
                {
                    if (t.Exception != null)
                    {
                        Exception ex = t.Exception.InnerExceptions[0];
                        if (!HandleLoadDenendentEntitiesException(ex))
                            throw ex;
                    }
                }
                else
                {
                    if (!t.IsCanceled)
                    {
                        dependentEntities.Refill(t.Result, restoreCurrent);

                        OnPropertyChanged(() => DependentEntities);
                        OnRefreshedDependentEntities(state);
                    }
                }

                if (complete != null)
                    complete.Invoke();
            }, TaskContinuationOptions.ExecuteSynchronously);
            //}, TaskScheduler.FromCurrentSynchronizationContext());
        }
    }
}
